Augment BooleanAnd falsey and BooleanOr truthy type narrowing when left and right conditions narrow different expression keys#5595
Open
phpstan-bot wants to merge 3 commits intophpstan:2.1.xfrom
Conversation
ebad75c to
d2744e3
Compare
…n left and right conditions narrow different expression keys
- In TypeSpecifier::specifyTypesInCondition for BooleanAnd falsey context,
the intersection of normalized left and right SpecifiedTypes drops all
narrowing when the two sides operate on different expression keys
(e.g. $test vs $test['hi']). Add augmentDisjunctionTypes() to recover
narrowing by computing both disjunction-path scopes and unioning the
narrowed types for any expression narrowed in both paths.
- Apply the same augmentation for BooleanOr truthy context, which is
the De Morgan dual and has the same information loss.
- Update four existing tests whose assertions reflected the old
incomplete narrowing: type-specifying-extensions-2-{false,null}.php
(extension fires in all contexts, both paths now narrow correctly),
bug-3632 (additional true-positive instanceof error detected),
bug-11903 (PHPDoc tip removed because narrowing is now from control
flow).
- Add regression tests covering the reported case (isset && is_string
on array union), the || dual, and parallel type checks (is_array,
is_int, is_float, is_bool, array_key_exists).
- Rename $leftNorm/$rightNorm to $leftNormalized/$rightNormalized - Rename $origType to $originalType - Extract $types->getSureTypes() into $existingSureTypes before the loop - Early-continue on left narrowing check before computing right narrowing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
d2744e3 to
f178bf5
Compare
VincentLanglet
approved these changes
May 10, 2026
Contributor
|
To me the integration test errors are correct |
Member
|
Please look at issue-bot results and decide whether anything else is fixed too |
Contributor
VincentLanglet
left a comment
There was a problem hiding this comment.
Add non regression for phpstan/phpstan#13061
(with the code snippet https://phpstan.org/r/923d55d8-296e-42ea-897b-0dffb23e4ef5)
Add non regression for phpstan/phpstan#7259
(with both the code snippet https://phpstan.org/r/dca4f949-ab14-41d0-b02d-09adc7ff011e and https://phpstan.org/r/ee55e085-aff1-4c4e-9ef6-96ceceec9e83)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Collaborator
Author
|
Pushed. Added two regression test files:
Both |
VincentLanglet
approved these changes
May 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When
isset($test['hi']) && is_string($test['hi'])was used in a combined&&condition with an early return, PHPStan failed to narrow the array union type in the falsey branch. Splitting the condition into nestedifblocks worked correctly. The fix recovers the lost type narrowing for bothBooleanAndfalsey and the De Morgan dualBooleanOrtruthy.Changes
augmentDisjunctionTypes()method tosrc/Analyser/TypeSpecifier.phpthat computes both disjunction-path filtered scopes and checks if candidate expressions are narrowed in both paths. If so, it creates a sureType with the union of the narrowed types.BooleanAndfalsey branch (after the existingintersectWith) using left-falsey and right-falsey-given-left-truthy scopes.BooleanOrtruthy branch (after the existingintersectWithandaugmentBooleanOrTruthyWithConditionalHolders) using left-truthy and right-truthy-given-left-falsey scopes.tests/PHPStan/Analyser/data/type-specifying-extensions-2-false.phpandtype-specifying-extensions-2-null.php:$foois now correctlystringinstead ofstring|nullbecause the test extension fires in all contexts and both disjunction paths narrow it.tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php(testBug3632): after progressive elimination by early returns,$ais now correctly narrowed tonulland$btoNiceClass, producing an additional true-positiveinstanceoferror at line 36.tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php(testBug11903): PHPDoc tip removed from line 21's error because the narrowing is now from control flow rather than PHPDoc types.Root cause
In
TypeSpecifier::specifyTypesInCondition, the falsey branch ofBooleanAnd(and truthy branch ofBooleanOr) usesSpecifiedTypes::intersectWith()to combine type assertions from the two operands.intersectWithonly keeps expression keys present in both operand sets. When the left condition narrows$test(the array variable) and the right condition narrows$test['hi'](an array offset), they have different expression keys, sointersectWithdrops both — resulting in no narrowing at all.The fix adds a post-intersection augmentation step that computes the two actual filtered scopes (one for each disjunction path) and checks if both paths narrow the same expression. If so, the union of the narrowed types is added as a sureType.
Analogous cases probed
augmentDisjunctionTypesmethod using truthy scopes instead of falsey scopes. Test added.is_array,is_int,is_float,is_boolall tested in combination withisset— all pass with the fix.array_key_exists: tested as an alternative toisset— passes.augmentBooleanOrTruthyWithConditionalHolders): already existed for BooleanOr truthy but only handles candidates from pre-existing conditional holders in the scope, not from the current condition's sureTypes. The new augmentation complements it.Test
tests/PHPStan/Analyser/nsrt/bug-14566.php: regression test for the reported bug withisset && is_stringonarray{}|array{hi: 'hello'}|array{hi: array{0: 42, 1?: 42}}, plus:&&with early return (the reported bug)&&with offset assignment (from the issue's playground)||dual case (!isset || !is_string)is_array,is_int,is_float,is_boolvariantsarray_key_existsvariantFixes phpstan/phpstan#14566
Fixes phpstan/phpstan#13061
Fixes phpstan/phpstan#7259